/*------------------------------------------------------------------------------
 *  Schematic - Schematic Object  
 *  
 *  August 2009, Forhad Ahmed
 *------------------------------------------------------------------------------
 */

package schematic;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.Point2D;
import java.awt.image.MemoryImageSource;
import java.util.Stack;
import java.util.Vector;

import ui.components.core.CComponent;


public class Schematic extends CComponent {
    /*
     * Constructor
     */
    public Schematic() {
        super(null);

        objects = new Vector();
        nodes = new Vector();
        edges = new Vector();
        junctions = new Vector();
        selection = new Vector();
        checkpoint = new Vector();
        undo = new Stack();
        redo = new Stack();      
        
        hit = null;
        tedge = null;
        sjunction = null;
        fjunction = null;
        suggestion = new Point(-1,-1);
        delta = new Point();
        center = new Point2D.Float();
        crosshair = new Point(-1, -1);
        last = new Point();
        ortho_point = new Point();
        box = new Rectangle();
        delete_list = new Vector();
        
        /*
         * Initialize the transparent cursor
         */
        if (img == null) {
            Toolkit toolkit = Toolkit.getDefaultToolkit();
            
            if (pixels == null) {
                pixels = new int[16 * 16];
            }
            
            MemoryImageSource mem = new MemoryImageSource(16,16, pixels, 0, 16);
            img = toolkit.createImage(mem);
            
            if (tcursor == null) {
                tcursor = toolkit.createCustomCursor(img, new Point(0,0), "tc");
            }
        }
        
        /*
         * We need this in order to recieve keyboard events
         */
        setFocusable(true);
        
        /*
         * Event handlers
         */
        mouse = new CanvasMouseAdapter(this);
        keyboard = new VCanvasKeyAdapter(this);
        addMouseListener(mouse);
        addMouseMotionListener(mouse);
        addKeyListener(keyboard);
    }
    
    
    /*
     * Set no cursor
     */
    protected void nocursor() {
        this.setCursor(tcursor);
    }
    
    
    /*
     * Set default mouse cursor
     */
    protected void pcursor() {
        this.setCursor(Cursor.getDefaultCursor());
    }
    
   
    /*
     * Dump the state of the schematic
     */
    public void dump() {
        
    }
   
    
    /*
     * Debug an object as it's added to the checkpoint
     */
    protected void debugCheckpoint(SObject obj) {
        String action;
        
        if (obj.checkpoint == 0) {
            action = " ! ";
        } else if (obj.checkpoint == 1) {
            action = " + ";
        } else {
            action = " - ";
        }
        
        System.out.println(action + obj.toString());
    }
    
    
    /*
     * Debug start/end of transaction
     */
    protected void debugTransaction() {
        String st = "--- TRANSACTION START --- ";
        String ft = "--- TRANSACTION FINIS --- ";
        
        if (transaction_state == TRANSACTION_START) {
            System.out.println();
            System.out.println(st+" ["+(transactions+1)+"]");
        } else {
            System.out.println(ft+" ["+transactions+"]");
            System.out.println();
        }
    }
    
    
    /*
     * Check to see if a given object is already checkpointed.
     */
    public boolean checkpointed(SObject original) {
        int index;
        SObject object;
        
        for (index = 0; index < checkpoint.size(); index++) {
            object = (SObject) checkpoint.get(index);
            if (object.id == original.id) return true;
        }
        return false;
    }
    
    /*
     * Add an object to be checkpointed
     */
    public void checkpoint(SObject object, int action) {
        SObject chkpt;   /* Copy of original object to be stored  */
        
        /*
         * We must have called transaction(start) before we started to
         * checkpoint() objects
         */
        assert (transaction_state == TRANSACTION_START);
        
        if (!checkpointed(object)) {  
            chkpt = object.checkpoint(action);
            chkpt.checkpoint = action; /* TODO: move this inside obj ckpt fn */
            checkpoint.add(chkpt);
            debugCheckpoint(chkpt);
        }
    }
    
    
    /*
     * Add a list of objects to be checkpointed.
     */
    public void checkpoint(Vector objects, int action) {
        int index;
        SObject object;
        
        if (objects == null) return;
        
        for (index = 0; index < objects.size(); index++) {
            object = (SObject) objects.get(index);
            checkpoint(object, action);
        }
    }

    
    /*
     * Uncheckpoint all checkpointed data and undo their effects because of a 
     * canceled transaction
     */
    public void uncheckpoint() {
        int index;
        SObject chkpt, original;
        
        /*
         * Go through the checkpointed object list and revert each action taken
         */
        for (index = 0; index < checkpoint.size(); index++) {
            /*
             * The checkpoint vector contains copies of the original objects, 
             * but with the same id.  We need to find the original object with
             * the same id.
             */
            chkpt = (SObject) checkpoint.get(index);
            original = chkpt.original;
            
            if (chkpt.checkpoint == Schematic.CHECKPOINT_ADD) {
                remove_internal(original);
            } else if (chkpt.checkpoint == Schematic.CHECKPOINT_MOD) {
                original.revert(chkpt);
            }
        }   
        checkpoint.clear();
    }
  
    
    /*
     * Start/stop a transaction.  All checkpointing must be done within a 
     * transaction window.
     */
    public void transaction(int st) {
        Vector diff;
        
        if (st == TRANSACTION_START) {
            checkpoint.clear();
            /*
             * Since this is the start of the transaction, we clear the chkpt 
             * objects and we will be calling checkpoint() on all objects that
             * need saving after this call to transaction(start).
             */
        } else {
            /*
             * Do all necessary cleanupup work here - adding objects to the 
             * checkpoint list - before taking the diff.  cleanup work includes 
             * things like merging of edges, flattening edges, etc. 
             */
            cleanup();
            
            diff = diff(checkpoint);
            
            /*
             * Filter transactions that had no objects changed
             */
            if (diff != null) {
                /*
                 * Keep the undo stack depth in check! Start removing older 
                 * stuff if we have to. 
                 */
                if (undo.size() >= undo_depth) undo.remove(0);
                undo.push(diff);
                redo.clear();
                transactions++;
            }
            
            checkpoint.clear();
            
            /*
             * XXX
             */
            System.gc();
        }
        
        assert (transaction_state != st);
        transaction_state = st;
        debugTransaction();
    }
    
    
    /*
     * Given the checkpoint vector, generate the diff vector.  The checkpoint
     * objects are each modified into diff objects and stored in the diff vector
     */
    protected Vector diff(Vector checkpoint) {
        Vector diff = new Vector();
        SObject chkpt;
        int index;
        
        if (checkpoint.size() == 0) return null;
        
        System.out.println("--- DIFF START ---");
        for (index = 0; index < checkpoint.size(); index++) {
            chkpt = (SObject) checkpoint.get(index);
            chkpt.diff(chkpt.original); /* Self modification of chkpt object */
            diff.add(chkpt);
        }
        System.out.println("--- DIFF FINIS ---");
          
        return diff;
    }
    
    
    /*
     * Undo last action if possible
     */
    public void undo() {
        Vector diffs;
        SObject diff;
        int index;
        
        /*
         * Very important to block any such operation while a transaction is 
         * taking place.
         */
        if (transaction_state == Schematic.TRANSACTION_START) return;
        
        if (undo.size() <= 0) return;
        
        System.out.println("--- UNDO ---");
        
        diffs = (Vector) undo.pop();
        
        for (index = 0; index < diffs.size(); index++) {
            diff = (SObject) diffs.get(index);
            diff.original.rpatch(diff, this);
        }
        
        redo.push(diffs);
            
        repaint();
        
        /*
         * XXX
         */
        System.gc();
    }
    
    
    /*
     * Redo last action if possible
     */
    public void redo() {
        Vector diffs;
        SObject diff;
        int index;
        
        /*
         * Very important to block any such operation while a transaction is 
         * taking place.
         */
        if (transaction_state == Schematic.TRANSACTION_START) return;
        
        if (redo.size() <= 0) return;
        
        System.out.println("--- REDO ---");
        
        diffs = (Vector) redo.pop();
        
        for (index = 0; index < diffs.size(); index++) {
            diff = (SObject) diffs.get(index);
            diff.original.patch(diff, this);
        }
        
        undo.push(diffs);
            
        repaint();   
        
        /*
         * XXX
         */
        System.gc();
    }
    
    
    /*
     * Cut the objects from the selection.  Do fixup if required
     */
    public void cut() {
        
    }
    
    
    /*
     * Copy the selected objects to the clipboard.
     */
    public void copy() {
        
    }
    
    
    /*
     * Paste the clipboard onto the schematic.
     */
    public void paste() {
        
    }
    
    
    /*
     * Do a wide variety of fixup tasks at the end of each transaction.
     */
    public void cleanup() {
        System.out.println("--- CLEANUP ---");
        cap();
        /*
         * bind coincidentals
         */
        merge();
        flatten();
        /*
         * All terminals matched with all orphaned junctions
         * All orphaned junctions matched with all junctions
         */
    }
    
    
    /*
     * For edges that have head/tail set to null as a result of deletion, we
     * cannot let these edges to continue having null as their end, so we 'cap'
     * the edge end with a junction;
     */
    public void cap() {
        int index;
        Object end;
        SEdge edge;
        SJunction junction;
        Point p;
        
        for (index = 0; index < edges.size(); index++) {
            edge = (SEdge) edges.get(index);
            
            if (edge.head == null) {
                end = edge.temp_head;
                p = edge.point(end);
                junction = new SJunction(p.x, p.y);
                edge.link(0, junction, this);
                checkpoint(junction, 1);
                add_internal(junction);
            }
            
            if (edge.tail == null) {
                end = edge.temp_tail;
                p = edge.point(end);
                junction = new SJunction(p.x, p.y);
                edge.link(1, junction, this);
                checkpoint(junction, 1);
                add_internal(junction);
            }
        } 
    }
    
    
    /*
     * Do the merge operation for junctions that have exactly 2 edges incident
     * upon them.
     */
    public void merge() {
        int index;
        SJunction junction;
        
        for (index = 0; index < junctions.size(); index++) {
            junction = (SJunction) junctions.get(index);
            
            if (junction.edges.size() == 0) {
                System.out.println("*** DELET: " + junction);
            } else if (junction.edges.size() == 2) {
                System.out.println("*** MERGE: " + junction);
            }
        }
    }
    
    
    /*
     * Flatten (remove coinciding points) from all edges 
     */
    public void flatten() {
        int index;
        SEdge edge;
        
        for (index = 0; index < edges.size(); index++) {
            edge = (SEdge) edges.get(index);
            if (!edge.flat()) {
                checkpoint(edge, 0);
                edge.flatten();
            }
        }
    }
    

    /*
     * Add an object to the schematic.
     */
    public void add(SObject object) {
        checkpoint(object, 1);
        add_internal(object);
    }
    
    
    /*
     * Internal add function that adds the object to the global objects list as
     * well as individual object type lists
     */
    public void add_internal(SObject object) {
        objects.add(object);
        
        /*
         * Based on the type of the object, keep it in their separate lists
         */
        if (object instanceof SNode) {
            nodes.add(object);
        } else if (object instanceof SEdge) {
            edges.add(object);
        } else if (object instanceof SJunction) {
            junctions.add(object);
        } 
        
        object.mark_deleted = false;
    }
    
    
    /*
     * Internal remove function that removes the object from the global objects
     * list as well as from the individual object type lists
     */
    public void remove_internal(SObject object) {
        objects.remove(object);
        
        /*
         * Based on the type of the object, keep it in their separate lists
         */
        if (object instanceof SNode) {
            nodes.remove(object);
        } else if (object instanceof SEdge) {
            edges.remove(object);
        } else if (object instanceof SJunction) {
            junctions.remove(object);
        }         
        
        /*
         * TODO: explain why we need to deselect
         */
        deselect(object);
    }
    
    
    /*
     * Reset the selection box
     */
    protected void resetSelectionBox() {
        box.setBounds(0, 0, 0, 0); 
    }
    
    
    /*
     * By drawing a selection box in the schematic, the 'selection' rectangle is 
     * updated, and once the selection is complete (user releases the mouse) 
     * the 'selection' rectangle is also complete.  This selectBox() function
     * selects all the objects that are in the 'selection' rectangle.
     * 
     * The algorithm is to first blindly sweep all objects and see if their
     * bounding boxes are enclosed inside the selection box.  All such objects
     * are *marked* as selected first (not actually selected). Then a second
     * sweep is done for each object in the selection and each object is checked
     * to see if it *can* be selected based on whether it's dependent objects 
     * are marked selected or not.
     */
    public void selectBox() {
        int index;
        SObject object;
        Rectangle s = box;
           
        s.x = s2c(box.x);
        s.y = s2c(box.y);
        s.width = s2c(box.width);
        s.height = s2c(box.height);
                
        /*
         * Since setting 'ctrl' to true allows for multiple selections, we need
         * to do this here because we want to select all objects inside the box.
         * We unset 'ctrl' immediate after selecting all objects 
         */
        ctrl = true;
        
        /*
         * Whether or not an edge can be selected by the selection box depends
         * on whether the edge's head/tail objects (either node terminal or 
         * junction) are also selected.  For this reason, we first do a mark of
         * all objects and apply this rule based on whether objects are marked
         */
        for (index = 0; index < objects.size(); index++) {
            object = (SObject) objects.get(index);
            if (boxContains(s, object)) object.mark_selected = true;
        }
        
        /*
         * Now that objects are marked selected, we can do a sweep of individual
         * object lists (in some order) to actually select objects.
         */
        for(index = 0; index < nodes.size(); index++) {
            object = (SObject) nodes.get(index);
            if (boxContains(s, object) && object.selectable()) 
                select(object, true);

        }
        for(index = 0; index < edges.size(); index++) {
            object = (SObject) edges.get(index);
            if (boxContains(s, object) && object.selectable()) 
                select(object, true);

        }
        for(index = 0; index < junctions.size(); index++) {
            object = (SObject) junctions.get(index);
            if (boxContains(s, object) && object.selectable()) 
                select(object, true);

        }

        
        ctrl = false;
    }
    
    
    /*
     * Given a rectangle, see if a given object will be selected by it
     */
    protected boolean boxContains(Rectangle box, SObject object) {
        Rectangle obox = null;
        
        obox = object.getBoundingBox();
        if (obox != null) {
            if (obox.width != 0 && obox.height != 0) {
                if (box.contains(object.getBoundingBox())) {
                    return true;
                }  
            } else {
                /*
                 * We have to deal with cases where the object's bounding 
                 * box is a degenerate rectangle (line/point) because the
                 * Rectangle class' contains(rect) method will return false
                 * for these cases
                 */
                if (obox.width == 0 && obox.height != 0) {
                    if (box.contains(obox.x, obox.y) && 
                            box.contains(obox.x, obox.y + obox.height)) {
                            return true;
                        }   
                } else if (obox.width != 0 && obox.height == 0) {
                    if (box.contains(obox.x, obox.y) && 
                        box.contains(obox.x + obox.width, obox.y)) {
                        return true;
                    }
                } else if (obox.width == 0 && obox.height == 0) {
                    if (box.contains(obox.x, obox.y)) {
                        return true;
                    }
                }
            }
        }        
        return false;
    }
    
    
    /*
     * Check to see if an object is selected
     */
    public boolean selected(SObject obj) {
        return obj.selected;
    }
    
    
    /*
     * Select an object on the schematic.  This adds to the 'selection' vector.  
     * The boolean 'selectall' is used for the 'CTRL+A' case where we have to
     * select all objects even if a subset of the objects are already selected:
     * we don't want to deselect that subset.
     */
    public void select(SObject obj, boolean selectall) { 
        /*
         * If CTRL is not pressed and the object we are trying to select now is
         * a new hit (not selected), then we deselect all, and just select the
         * new hit.
         */
        if (!ctrl  && !selected(obj)) {
            deselectAll();
        }
        
        if (selection.contains(obj) == false) {
            obj.selected = true;
            obj.mark_selected = true;
            selection.add(obj);
        } else {
            if (ctrl && !selectall) {
                deselect(obj);
            }
        }
    }
    
    
    /*
     * De-select an object from the 'selection' vector
     */
    public void deselect(SObject obj) {
        obj.selected = false;
        obj.mark_selected = false;
        selection.remove(obj);
             
    }
    
    
    /*
     * Sanity check
     */
    public void check() {
        int nselected = 0;
        int index;
        SObject object;
        
        /*
         * Right now just check the selection inconsistency
         */
        for (index = 0; index < objects.size(); index++) {
            object = (SObject) objects.get(index);
            if (object.selected) {
                nselected++;
                if (!selection.contains(object)) {
                    System.out.println("SELECTION INCONSISTENCY: " + 
                    object + "marked selected but not in selection list");
                }
            }
        }
        for (index = 0 ; index < selection.size(); index++) {
            object = (SObject) selection.get(index);
            if (!object.selected) {
                System.out.println("SELECTION INCONSISTENCY: " + 
                object + " not marked selected but in selection list");
            }
        }
        if (nselected != selection.size()) {
            System.out.println("SELECTION INCONSISTENCY: " +
                               "[marked selected: " + nselected + "] " + 
                               "[in selection list: " + selection.size() + "]");
        }
    }
    

    /*
     * De-select all objects from the schematic (for example, when clicking on 
     * the schematic outside of any objects).
     */
    public void deselectAll() {
        SObject obj;
        while (selection.size() > 0) {
            obj = (SObject) selection.get(0);
            deselect(obj);
        }
    }
    
    
    /*
     * Select all objects (CTRL + A)
     */
    public void selectAll() {
        int index;
        SObject obj;
        
        for (index = 0; index < objects.size(); index++) {
            obj = (SObject) objects.get(index);
            select(obj, true);
        }
    }
    
    /*
     * When moving a set of objects (such as the set of objects currently 
     * selected), we move the object that's being dragged (the leader) and the
     * others in the selection as well.  Moving the leader will offset the
     * leader by some delta, and we used that delta to move the others.
     * 
     * Some types of objects cannot be leaders (edges, junctions).  If we are
     * moving those, then their place() functions should return null for the 
     * delta point so that moveSelection knows not to move the rest of the 
     * selection when non-leader objects are moved.
     */
    protected Point place(SObject obj, int x, int y) {
        return obj.place(x, y);
    }
    
    
    /*
     * Move an object based on a delta
     */
    protected void move(SObject obj, int dx, int dy) {
        obj.move(dx, dy);
    }
    
    
    /*
     * Move the currently selected objects to a new location as a result of an 
     * object being dragged by the mouse, given the hit context from the first 
     * time the object was hit.
     */
    public void moveSelection(SObject leader, int x, int y) {
        int index;
        Point d;
        SObject object;
        boolean checkpoint = false;
        Vector dependents;
        
        if (!move) {
            /*
             * Move started. Checkpoint the objects that are move candidates.
             */
            move = true;
            checkpoint = true;
            transaction(0);
        }   
        
        /*
         * First we grab the dependents of the leader and tell each dependent to 
         * save some current state before the leader is moved.  We save the move
         * state only during the first move (checkpoint). 
         */
        dependents = leader.dependents();
        if (checkpoint) checkpoint(dependents, 0);
        if (checkpoint) saveDependentsMoveState(leader, dependents);
        
        /*
         * Move the leader to see how much the delta was.  Checkpoint if
         * necessary
         */
        if (checkpoint) checkpoint(leader, 0);
        leader.setMoveContext(selection);
        d = place(leader, x, y);

        /*
         * Now that we have a delta, move the dependents
         */
        if (d != null) moveDependents(leader, dependents, d.x, d.y);
        
        /*
         * Apply the move delta to all other selected items IF the leader was
         * part of the selected items.  Checkpoint the selection if necessary.
         */
        if (selection.contains(leader) && d != null) {
            for (index = 0; index < selection.size(); index++) {
                if (selection.get(index) != leader) {
                    /*
                     * First checkpoint/move dependents
                     */
                    object = (SObject) selection.get(index);
                    dependents = object.dependents();
                    if (checkpoint) checkpoint(dependents, 0);
                    if (checkpoint) saveDependentsMoveState(object, dependents);
                   
                    /*
                     * Then checkpoint/move the object itself
                     */
                    if (checkpoint) checkpoint(object, 0);
                    object.move(d.x, d.y);
                    
                    /*
                     * Move the dependents
                     */
                    moveDependents(object, dependents, d.x, d.y);
                }
            }
        }  
    }
    
    
    /*
     * Move the selected set of objects by a given delta.  In this case, we just
     * apply the delta to all selected objects as well as all dependents of the
     * objects
     */
    public void moveSelection(int dx, int dy) {
        int index;
        Point d;
        SObject object;
        boolean checkpoint = false;
        Vector dependents;
        
        if (dx == 0 && dy == 0) return;
        if (selection.size() == 0) return;
        
        if (!move) {
            /*
             * Move started. Checkpoint the objects that are move candidates.
             */
            move = true;
            checkpoint = true;
            transaction(0);
        }
        
        /*
         * Apply the move delta to all other selected items. Checkpoint the 
         * selection if necessary.
         */
        for (index = 0; index < selection.size(); index++) {
            object = (SObject) selection.get(index);
            
            /*
             * First checkpoint dependents
             */
            dependents = object.dependents();
            if (checkpoint) checkpoint(dependents, 0);
            if (checkpoint) saveDependentsMoveState(object, dependents);
            
            /*
             * Then checkpoint/move the object itself
             */
            if (checkpoint) checkpoint(object, 0);
            object.move(dx, dy);
            
            /*
             * Then move the dependents
             */
            moveDependents(object, dependents, dx, dy);
            

        }
    }
    
    
    /*
     * Given a list of dependent objects for a parent object that is moving, 
     * move the dependent objects in response to the parent object move.
     */
    public void moveDependents(SObject p,  Vector deps, int dx, int dy) {
        int index;
        SObject object;
        
        if (deps == null) return;
        
        for (index = 0; index < deps.size(); index++) {
            object = (SObject) deps.get(index);
            object.subordinateMove(p, dx, dy);
        }
    }
    
    
    /*
     * Given a list of dependent objects for a parent object, save any relevant
     * state (attach-orientation) in those dependent objects before a parent 
     * object is changed (used after the change to parent has been made, when 
     * the dependents need to be changed as a result of parent's change.  
     */
    public void saveDependentsMoveState(SObject parent, Vector deps) {
        int index;
        SObject object;
        
        if (deps == null) return;
        
        for (index = 0; index < deps.size(); index++) {
            object = (SObject) deps.get(index);
            object.saveMoveState(parent);
        }
    }
    
    
    /*
     * Mouse has been released and move selection has ended.  Do checkpointing,
     * transaction, etc.
     */
    public void finishMoveSelection() {
        /*
         * Move ended.  End the move transaction.
         */
        move = false;
        transaction(1);
    }
    
    
    /*
     * Delete the selected list of objects from the schematic.  Doing a delete 
     * of a set of objects can potentially affect other objects (that are not 
     * being deleted) in the schematic (i.e. the dependents of the deleted 
     * objects).  We have to make sure those objects are handled properly.
     */
    public void deleteSelection() {
        int index;
        SEdge   edge;
        SObject obj;
        SObject head, tail;
        
        if (selection.size() == 0) return;
        
        transaction(0);
        
        /*
         * First clear the deleted flag from all objects
         */
        for (index = 0; index < objects.size(); index++) {
            obj = (SObject) objects.get(index);
            obj.mark_deleted = false;
        }
        
        /*
         * Mark the selected objects as deleted
         */
       
        for (index = 0; index < selection.size(); index++) {
            obj = (SObject) selection.get(index);
            obj.mark_deleted = true;
        }
        
        /*
         * Now that the delete objects are marked, analyze each one to see if 
         * they can really be deleted (based on dependent objects being marked)
         * and put them in the delete list 
         */
        delete_list.clear();
        for (index = 0; index < selection.size(); index++) {
            obj = (SObject) selection.get(index);
            if (obj.deletable()) {
                delete_list.add(obj);
            } else {
                obj.mark_deleted = false;
            }
        }
                
        /*
         * Since edges are the main objects where linking/unlinking take place
         * (edges are the 'glue' that stiches the schematic objects together), 
         * we sweep through the edges to do unlinking for the deletion
         */
        for (index = 0; index < edges.size(); index++) {
            edge = (SEdge) edges.get(index);
            head = edge.endobject(edge.head);
            tail = edge.endobject(edge.tail);
            
            if (edge.mark_deleted) {
                if (!head.mark_deleted) {
                    checkpoint(head, 0);
                    edge.unlink(0, edge.head, false, true);
                }
                if (!tail.mark_deleted) {
                    checkpoint(tail, 0);
                    edge.unlink(1, edge.tail, false, true);
                }
            } else {
                if (head.mark_deleted) {
                    checkpoint(edge, 0);
                    edge.unlink(0, edge.head, true, false);
                }
                if (tail.mark_deleted) {
                    checkpoint(edge, 0);
                    edge.unlink(1, edge.tail, true, false);
                }
            }
        }
        
        /*
         * Finally, remove the selected objects IF they are deletable()
         */
        for (index = 0; index < delete_list.size(); index++) {
            obj = (SObject) delete_list.get(index);
            checkpoint(obj, -1);
            remove_internal(obj);
        }
        
        transaction(1);
    }
    
    
    /*
     * Rotate a set of objects
     */
    public void rotateSelection(int dir) {
        SObject object;
        boolean checkpoint = false;
        int index;
        int rx, ry;
       
        
        if (selection.size() == 0) return;
        if (!selectionContained()) return;
   
        if (!rotate) {
            /*
             * Rotation started. Checkpoint the objects.
             */
            rotate = true;
            checkpoint = true;
            transaction(0);
        }   
        
        if (selection.size() == 1) {
            object = (SObject) selection.get(0);
            if (checkpoint) checkpoint(object, 0);
            object.rotate(dir);
        } else {
            /*
             * Selection contains multiple objects.  If these objects are 
             * capable of being rotated (the selection is 'self contained') then 
             * find the center of mass for all these objects and rotate them 
             * around this point.
             */
            if (checkpoint) {
                /*
                 * Calculate the center of mass point only initially to minimize
                 * roundoff errors.  TODO: find a way to eliminate these errors
                 */
                center.x = 0; center.y = 0;
                for (index = 0; index < selection.size(); index++) {
                    object = (SObject) selection.get(index);
                    center.x += object.center().x;
                    center.y += object.center().y;
                }
                center.x /= (float)selection.size();
                center.y /= (float)selection.size();
            }
           
            for (index = 0; index < selection.size(); index++) {
                object = (SObject) selection.get(index);
                if (checkpoint) checkpoint(object, 0);
                object.rotate(dir, Math.round(center.x), Math.round(center.y));
            }
        }
    }
    
    
    /*
     * Called when a rotation operation is finished, and we want to save the
     * transaction
     */
    public void finishRotateSelection() {
        /*
         * Rotation ended.  End the rotation transaction.
         */
        rotate = false;
        transaction(1);
    }
    
    
    /*
     * Check to see if selection is self contained;
     */
    public boolean selectionContained() {
        int index, j;
        Vector deps, objs;
        SObject object, d;
        
        objs = selection;
        
        for (index = 0; index < objs.size(); index++) {
            object = (SObject) objs.get(index);
            deps = object.dependents();
            if (deps != null) {
                for (j = 0; j < deps.size(); j++) {
                    d = (SObject) deps.get(j);
                    if (!d.selected) return false;
                }
            }
        }
        return true;
    }
    
    
    /*
     * Set the state of the schematic to NORMAL
     */
    public void setNormal() {
        pcursor();
        state = STATE_NORMAL;
        suggestion = null;
        hit        = null;
        tedge      = null;
        tnode      = null;
        sjunction  = null;
        fjunction  = null;
    }
    
    
    /*
     * Change the state of the schematic to EDGE.  We just selected the edge 
     * tool but we have NOT yet started an edge.
     */
    public void startEdge() {
        nocursor();
        state = STATE_EDGE;
        started = false;  
    }
    
    
    /*
     * Whether we have started an edge (in which case it is still unfinished) or
     * we have only selected the edge tool, we now want to cancel (right click).
     * Set the state back to normal, and if we did start an edge, then we also
     * started a transaction so now nullify the transaction.
     */
    public void cancelEdge() {
        /*
         * Edge finished prematurely.
         */
        setNormal();
        if (started) {
            uncheckpoint();
            transaction(1);
            started = false;
        }
    }
    
    
    /*
     * Point was added while in edge mode.  If we have not yet started an edge,
     * start an edge here.  If we are in the middle of creating an edge, add
     * points to the edge
     */
    public void addEdgePoint(int sx, int sy, int clicks) {
        SObject clicked;
        Point clickp;
        int cx, cy;
        
        cx = s2cr(sx);
        cy = s2cr(sy);
        clicked = suggestion(sx, sy);
        clickp = SEdge.getSuggestionPoint(clicked);
        
        /*
         * When a mouse click happens near a suggestion point (even if the click
         * is not exactly on the suggestion point in schematic coordinates, the 
         * edge point will be added to the suggestion point.  In this case, the
         * ortho point might be a little off.  We set the ortho point to the 
         * suggestion point in this case (if it's near the suggestion point).
         * NOTE: if the mouse is clicked in the middle of nowhere (i.e. we have
         * no suggestion point) then we just use the click point.
         */
        if (clickp != null) {
            ortho_point.x = clickp.x; 
            ortho_point.y = clickp.y;
        } else {
            ortho_point.x = cx; 
            ortho_point.y = cy;
        }
        
        if (tedge == null && !started) {
            /*
             * Check to see if any existing objects are sitting where the edge
             * is being started:
             * 
             * - If there is a node terminal, use that as the edge head.
             * - If there is a junction, use that as the edge head.
             * - If there is nothing, create a new junction and use that as head
             *   ONLY if double-clicked
             */            
            if (clicked != null) {
                transaction(0);
                started = true;
                nocursor();
                
                tedge = new SEdge();
                tedge.temp.x = cx;
                tedge.temp.y = cy;
                checkpoint(clicked, 0);
                tedge.link(0, clicked, this);
                
            } else {
                if (clicks >= 2) {
                    /*
                     * Start edge at new orphaned junction
                     */
                    transaction(0);
                    started = true;
                    nocursor();
                    
                    sjunction = new SJunction(cx, cy);
                    tedge = new SEdge();
                    tedge.temp.x = cx;
                    tedge.temp.y = cy;
                    
                    tedge.link(0, sjunction, null);
                    
                    add_internal(sjunction);
                    checkpoint(sjunction, 1);
                }
            }
        } else {
            /*
             * Check to see if any existing objects are sitting where the edge
             * is being extended:
             * 
             * - If there is a node terminal, use that as the edge tail.
             * - If there is a junction, use that as the edge tail.
             * - If there is nothing, create a new edge internal point
             * - If there is nothing AND dbl-click, end the edge with a junction
             */
            if (clicked != null) {
                checkpoint(clicked, 0);
                
                /*
                 * Linking the end tail with whatever is clicked might fail.
                 */
                if (tedge.link(1, clicked, this)) {
                    add_internal(tedge);
                    checkpoint(tedge, 1);
                    
                    tedge = null;
                    pcursor();
                    started = false;
                    transaction(1);
                }
                
            } else {
                if (clicks >= 2) {
                    /*
                     * Finish the edge in new orphaned junction.
                     */
                    fjunction = new SJunction(cx, cy);
                    assert(tedge != null);
                    
                    if (tedge.link(1, fjunction, null)) {  
                        /* 
                         * Since the first click of the mouse would have added 
                         * an internal point, and then the second click would 
                         * have added the junction, the edge will actually have 
                         * an body point coinciding with the tail junction! We 
                         * have to take care of this here before we add the edge 
                         * to the schematic.
                         */
                        tedge.fixtail();
                        
                        add_internal(tedge);
                        add_internal(fjunction);
                        checkpoint(tedge, 1);
                        checkpoint(fjunction, 1);
                               
                        tedge = null;
                        sjunction = null;
                        fjunction = null;
                        
                        started = false;
                        pcursor();
                        transaction(1);
                    }
                } else  {
                    /*
                     * Add point to the edge if the edge supports multipoint
                     */
                    if (tedge.multipoint) {
                        tedge.addPoint(cx, cy);
                    }
                }
            }
        }
        repaint();
    }
    
    
    /*
     * Start the NODE mode for the schematic, given a node object.
     */
    public void startNode(SNode node) {
        tnode = node;
        tnode.preview = true;
        tnode.place(-1000, -1000);
        
        nocursor();
        state = STATE_NODE;
    }
    
    
    /*
     * When we are in NODE mode, and we click the schematic, this function adds 
     * a copy of the temp node to the schematic.  This is primarily how nodes 
     * are added to the schematic
     */
    public void addTempNode() {
        SNode node = tnode.clone();
        transaction(0);
        checkpoint(node, 1);
        add_internal(node);
        transaction(1);
    }
    
    
    /*
     * When an object is hit, we need to record the hit context of that object 
     * so that subsequent drags of that object can take into account
     * the hit context
     */
    public void setHitContext(SObject obj, int cx, int cy) {
        System.out.println(obj.dump());
        obj.setHitContext(cx, cy);
    }
    
    
    /*
     * Get the object that is hit by a mouse click at a particular screen point
     */
    protected SObject hit(int sx, int sy) {
        int index;
        SObject object;
        
        hit = null;
        
        /*
         * Search the objects that might contain this point.  There is an order
         * in which we select the objects: junctions, nodes, edges... so we must
         * search their lists separately.
         */
        for (index = 0; index < junctions.size(); index++) {
            object = (SObject) junctions.get(index);        
            if (object.hit(sx, sy)) {
                hit = object; 
                return hit;
            }
        }
        for (index = 0; index < nodes.size(); index++) {
            object = (SObject) nodes.get(index);        
            if (object.hit(sx, sy)) {
                hit = object; 
                return hit;
            }
        }
        for (index = 0; index < edges.size(); index++) {
            object = (SObject) edges.get(index);        
            if (object.hit(sx, sy)) {
                hit = object; 
                return hit;
            }
        }
        
        /*
         * The move boolean mostly applies to hit objects for calculating first
         * offset.  We set move to false here.
         */
        return hit;
    }
    
    
    /*
     * When in EDGE mode, given a schematic coordinate, find the (sub)object 
     * nearest that point.  The object returned may be of various types and the
     * VCanvasEdge's getSuggestionPoint(suggestion) API will return the exact
     * point for the object being suggested.
     */
    protected SObject suggestion(int x, int y) {
        int index;
        SObject object;
        SObject suggestion = null;
        
        
        /*
         * Search the objects that might suggest this point.  There is an order
         * in which we select the objects: junctions, nodes, edges... so we must
         * search their lists separately.
         */
        for (index = 0; index < junctions.size(); index++) {
            object = (SObject) junctions.get(index);
            suggestion = object.suggest(x, y);
            if (suggestion != null) return suggestion;
        }
        for (index = 0; index < nodes.size(); index++) {
            object = (SObject) nodes.get(index);
            suggestion = object.suggest(x, y);
            if (suggestion != null) return suggestion;
        }
        for (index = 0; index < edges.size(); index++) {
            object = (SObject) edges.get(index);
            suggestion = object.suggest(x, y);
            if (suggestion != null) return suggestion;
        }
        
        return null;
    }
    
    
    /*
     * We are in the EDGE state and we have either not started an edge or 
     * started an edge.  Moving the mouse anywhere now will cause us to look for
     * a 'suggestion' point for the edge.  If we haven't yet started the edge, 
     * then this suggestion point will be where the edge will start.  If we have
     * started an edge, this suggestion point will be an addition internal point
     * in the edge.
     */
    protected void suggestEdgePoint(int sx, int sy) {
        SObject suggestion = null;
        
        suggestion = suggestion(sx, sy);  
        
        if (suggestion != null) {
            this.suggestion = SEdge.getSuggestionPoint(suggestion);
        } else {
            this.suggestion = null;
        }
    }
    
   
    /*
     * Draw the suggestion point if we are in the EDGE state.
     */
    public void drawSuggestion(Graphics g) {
        g.setColor(suggestion_color);
        if (this.suggestion != null && state == STATE_EDGE) {
            if (suggestion.x >= 0) {
                g.fillRect(c2s(suggestion.x)-2, c2s(suggestion.y)-2, 5, 5);
            }
        }
    }
    
    
    /*
     * Draw cross-hair for aiding in edge placement
     */
    public void drawCrossHair(Graphics2D g) {
        g.setColor(crosshair_color);
        if (state == STATE_EDGE) {
            g.setStroke(dash);
            g.drawLine(c2s(crosshair.x), 0, c2s(crosshair.x), getHeight());
            g.drawLine(0, c2s(crosshair.y), getWidth(), c2s(crosshair.y));
        }
    }
    
    
    /*
     * Draw temporary objects (temporary edge during edge creation or temporary
     * node during node creation)
     */
    public void drawTemp(Graphics g) {
        if (state == STATE_EDGE) {
            if (started && tedge != null) {
                tedge.draw(g, this);
            }
        } 
        if (state == STATE_NODE) {
            tnode.draw(g, this);
        }
    }
    

    /*
     * Draw the background of the schematic
     */
    public void drawBackground(Graphics g) {
        g.setColor(background_color);
        g.fillRect(0, 0, getWidth(), getHeight());
    }
    
    
    /*
     * Draw the schematic grid
     */
    public void drawGrid(Graphics g) {    
        g.setColor(grid_color);
        
        if (grid) {
            int x, y, mx, my, px, py;
            mx = s2c(getWidth());
            my = s2c(getHeight());
            
            for (x = 0; x < mx; x += spacing) {
                for (y = 0; y < my; y += spacing) {
                    px = c2s(x);
                    py = c2s(y);
                    g.drawLine(px, py, px, py);
                }
            }
        }
    }
    
    
    /*
     * Draw the selection box
     */
    public void drawSelectionBox(Graphics g) {
        g.setColor(selection_box_color);
        if (state == STATE_SELECTION) {
            g.drawRect(box.x,     box.y, 
                       box.width, box.height);
        }
    }
    
    
    /*
     * Draw all schematic objects.  We have to draw the objects in a certain 
     * order: nodes, edges, junctions
     */
    public void drawObjects(Graphics g) {
        int index;
        SObject object;
        
        for (index = 0; index < nodes.size(); index++) {
            object = (SObject) nodes.get(index);
            object.draw(g, this);
        }
        for (index = 0; index < edges.size(); index++) {
            object = (SObject) edges.get(index);
            object.draw(g, this);
        }
        for (index = 0; index < junctions.size(); index++) {
            object = (SObject) junctions.get(index);
            object.draw(g, this);
        }
    }
    
    
    /*
     * Paint the schematic
     */
    public void paint(Graphics g) {
        
        Graphics2D g2 = (Graphics2D) g;

        drawBackground(g);
        
        drawGrid(g);
        
        drawObjects(g);
        
        drawSuggestion(g);
        
        drawTemp(g);
        
        drawCrossHair(g2);

        drawSelectionBox(g);
        
        /*
         * XXX
         */
        check();
    }
    
    
    /*
     * Set the zoom factor for the schematic
     */
    public void zoom(int z) {
        if (z > -VCANVAS_UNIT) {
            zoom = z;
            repaint();
        }
    }
    
    
    /*
     * Get schematic coordinates from screen coordinates. Returned schematic \
     * coord is a floor value.  Use s2cr() for rounded value.
     */
    public static int s2c(int s) {
        return  s / (VCANVAS_UNIT + zoom);
    }

    
    /*
     * Get rounded schematic coordinates from screen coordinates.  Use when need 
     * to get nearest schematic coord from screen coord (Edge drawing).
     */
    public static int s2cr(int s) {
        return Math.round((float)s / (float)(VCANVAS_UNIT + zoom));
    }
    
    
    /*
     * Get screen coordinates from schematic coordinates
     */
    public static int c2s(int c) {
        return c * (VCANVAS_UNIT + zoom);
    }
    
    
    /*
     * Colors
     */
    protected static Color background_color     = Color.black;
    protected static Color selection_box_color  = Color.orange;
    protected static Color selection_color      = Color.red;
    protected static Color grid_color           = new Color(90, 90, 90);
    protected static Color bounds_color         = new Color(70, 70, 70);
    protected static Color node_color           = Color.cyan;
    protected static Color suggestion_color     = Color.red;
    protected static Color crosshair_color      = Color.gray;
    
    /*
     * Zoom factor (-VCANVAS_UNIT + 1 ... N)
     */
    protected static int zoom = 1;
    
    /*
     * Canvas grid related
     */
    protected boolean grid = true;
    protected int     spacing = 10;
    
    /*
     * Dashed lines
     */
    protected static Stroke dash = new BasicStroke(1, BasicStroke.CAP_BUTT,
                                                      BasicStroke.JOIN_BEVEL, 0,
                                                      new float[] {1, 2}, 0);
    
    /*
     * Transparent cursor related
     */
    public static int[]  pixels = null;
    public static Image  img = null;
    public static Cursor tcursor = null; 

    /*
     * Crosshair related
     */
    protected Point   crosshair;
    public boolean    orthogonal = false;
    public Point      ortho_point;
    public int        ortho_dir  = 0;
    public static int VCANVAS_ORTHO_N = 0;
    public static int VCANVAS_ORTHO_H = 1;
    public static int VCANVAS_ORTHO_V = 2;

    /*
     * Canvas unit in pixels
     */
    protected static int VCANVAS_UNIT = 4;
    
    /*
     * Canvas state related
     */
    protected int     state = STATE_NORMAL;
    protected boolean move = false;
    protected boolean rotate = false;
    protected boolean started = false;
    
    /*
     * Selection related
     */
    protected Point     last;
    protected Rectangle box;
    protected Vector    selection;
    protected boolean   ctrl = false;
    
    /*
     * Undo/Redo related 
     */
    protected Vector checkpoint;
    protected Stack  undo;
    protected Stack  redo;
    protected int    undo_depth = 100000000;
    
    /*
     * Static clipboard shared across all schematic objects
     */
    protected static Vector clipboard;
    
    /*
     * Canvas contents.  All objects are kept in the 'objects' list, but we do 
     * maintain separate lists based on object type (node/edge/junction) because 
     * sometimes we have want to iterate over a specific type only, etc.
     */
    protected Vector objects;
    protected Vector nodes;
    protected Vector edges;
    protected Vector junctions;
    
    /*
     * Event handlers
     */
    protected MouseAdapter mouse;
    protected KeyAdapter   keyboard;
    
    /*
     * Temporary variables used for return values. NOT thread-safe!
     */
    protected SObject   hit;
    protected SEdge     tedge;
    protected SNode     tnode;
    protected SJunction sjunction;
    protected SJunction fjunction;
    protected Point     suggestion;
    protected Point     delta;
    protected Vector    delete_list;
    
    
    protected Point2D.Float center;
    
    /*
     * State enumerations 
     */
    protected static int STATE_NORMAL     = 0;
    protected static int STATE_NODE       = 1;
    protected static int STATE_EDGE       = 2;
    protected static int STATE_SELECTION  = 3;
    protected static int STATE_HIT        = 4;
    
    /*
     * Checkpoint/Transaction related
     */
    protected static int TRANSACTION_START   = 0;
    protected static int TRANSACTION_END     = 1;
    protected static int CHECKPOINT_MOD      = 0;
    protected static int CHECKPOINT_ADD      = 1;
    protected static int CHECKPOINT_DEL      = -1;
    protected static int CHECKPOINT_ORIGINAL = -2;
    public int           transactions = 0;
    public int           transaction_state = TRANSACTION_END;
}



/*
 * Canvas mouse handler
 */
class CanvasMouseAdapter extends MouseAdapter {
    public CanvasMouseAdapter(Schematic c) {
        sch = c;
    }
    
    public void mouseClicked(MouseEvent e) {
        int cross_x, cross_y;
        

        if (sch.state == Schematic.STATE_EDGE) {
            cross_x = Schematic.c2s(sch.crosshair.x);
            cross_y = Schematic.c2s(sch.crosshair.y);

            if (e.getButton() == MouseEvent.BUTTON1) {
                sch.addEdgePoint(cross_x, cross_y, e.getClickCount());
            } else if (e.getButton() == MouseEvent.BUTTON3) {
                /*
                 * If we right-clicked while forming an edge, cancel the edge.
                 */
                sch.cancelEdge();
            }
        } else if (sch.state == Schematic.STATE_NODE) {
            if (e.getButton() == MouseEvent.BUTTON1) {
                sch.addTempNode();
            } else if (e.getButton() == MouseEvent.BUTTON3) {
                sch.setNormal();
            } 
        }
    }

    public void mousePressed(MouseEvent e) {
        int cx, cy;
        SObject hit = null;
        
        cx = Schematic.s2cr(e.getX());
        cy = Schematic.s2cr(e.getY());
        
        if (sch.state == Schematic.STATE_NORMAL) {
            /*
             * Check on a mouse press are the objects that are hit
             */
            hit = sch.hit(e.getX(), e.getY());
            sch.last = e.getPoint();
            
            if (hit != null) {
                 sch.state = Schematic.STATE_HIT;             
                 sch.select(hit, false);
                 sch.setHitContext(hit, cx, cy);
            } else {
                hit = null;
                if (!sch.ctrl) sch.deselectAll();
            }
        } else if (sch.state == Schematic.STATE_EDGE) {
            /*
             * Handled by mouseClick().
             */
        }
        sch.repaint();
    }

    public void mouseReleased(MouseEvent e) {
        
        if (sch.state == Schematic.STATE_NORMAL) {
            
        } else if (sch.state == Schematic.STATE_SELECTION) {
            
            sch.selectBox();
            sch.resetSelectionBox();
            sch.setNormal();
            
        } else if (sch.state == Schematic.STATE_HIT) {
            if (sch.move) sch.finishMoveSelection();
            sch.setNormal();
        }
        sch.repaint();
    }

    public void mouseEntered(MouseEvent e) {
        sch.grabFocus();
    }

    public void mouseExited(MouseEvent e) {
        
    }

    public void mouseWheelMoved(MouseWheelEvent e) {
        
    }

    public void mouseDragged(MouseEvent e) {

        if (sch.state == Schematic.STATE_NORMAL) {
            
            sch.state = Schematic.STATE_SELECTION;
            
        } else if (sch.state == Schematic.STATE_SELECTION) {
            /*
             * Construct the 'selection' rectangle based on whether we dragged
             * negative values relative to the 'last' point.
             */
            int x, y, x1, y1, x2, y2, w, h;
            
            x1 = sch.last.x; y1 = sch.last.y;
            x2 = e.getX(); y2 = e.getY();
            
            w = (x1 - x2 < 0) ? (x2 - x1) : (x1 - x2);
            h = (y1 - y2 < 0) ? (y2 - y1) : (y1 - y2);
            x = (x2 > x1) ? x1 : x2;
            y = (y2 > y1) ? y1 : y2;

            sch.box.setBounds(x, y, w, h);
            
        } else if (sch.state == Schematic.STATE_HIT) {
            /*
             * Mouse is being dragged while some object was hit.  This will 
             * result in dragging of the hit object or dragging of a whole set
             * of objects if multiple objects are pre-selected.
             */
            int x, y;
            x = Schematic.s2cr(e.getX()); 
            y = Schematic.s2cr(e.getY());
            
            sch.moveSelection(sch.hit, x, y);
        }      
        sch.repaint();
    }

    public void mouseMoved(MouseEvent e) {
        int cxr, cyr, ox, oy, scx, scy, h, v;
        
        cxr  = Schematic.s2cr(e.getX());
        cyr  = Schematic.s2cr(e.getY());
        ox   = sch.ortho_point.x;
        oy   = sch.ortho_point.y;
        scx  = Schematic.c2s(sch.crosshair.x);
        scy  = Schematic.c2s(sch.crosshair.y);
        
        if (sch.state == Schematic.STATE_EDGE) {
            /*
             * Track the schematic crosshair with the current mouse point. 
             * Filter the x/y components if crosshair movement is supposed to be 
             * orthogonal
             */
            if (sch.orthogonal) {
                if (sch.started && sch.tedge != null) {

                    if (sch.ortho_dir == Schematic.VCANVAS_ORTHO_H) cyr = oy;
                    if (sch.ortho_dir == Schematic.VCANVAS_ORTHO_V) cxr = ox;
                    
                    if (cxr == ox && cyr != oy) {
                        sch.ortho_dir = Schematic.VCANVAS_ORTHO_V;
                    }
                    if (cxr != ox && cyr == oy) {
                        sch.ortho_dir = Schematic.VCANVAS_ORTHO_H;
                    }
                    if (cxr != ox && cyr != oy) {
                        sch.ortho_dir = Schematic.VCANVAS_ORTHO_H;
                    }
                    
                    if (cxr == ox && cyr == oy) {
                        sch.ortho_dir = Schematic.VCANVAS_ORTHO_N;
                    }
                }
            }
 
            sch.crosshair.x = cxr;
            sch.crosshair.y = cyr;           
            
            sch.suggestEdgePoint(scx, scy);
            if (sch.started && sch.tedge != null) {
                /*
                 * Set the temporary end of the edge being worked on so that we
                 * can see the temporary edge as we move the mouse
                 */
                sch.tedge.temp.x = sch.crosshair.x;
                sch.tedge.temp.y = sch.crosshair.y;
            }
        } else if (sch.state == Schematic.STATE_NODE) {
            
            
            sch.tnode.place(cxr, cyr);
        }
        sch.repaint();
    }
    
    protected Schematic sch;
}


/*
 * Canvas keyboard handler
 */
class VCanvasKeyAdapter extends KeyAdapter {
    
    public VCanvasKeyAdapter(Schematic c) {
        sch = c;
    }
    
    public void keyTyped(KeyEvent e) {
        
    }
    
    protected boolean keytext(String s) {
        if (event != null) {
            return KeyEvent.getKeyText(event.getKeyCode()).equals(s);
        }
        return false;
    }
    
    protected boolean keychar(char c) {
        if (event != null) {
            return event.getKeyChar() == c;
        }
        return false;
    }

    public void keyPressed(KeyEvent e) {
        event = e;
        
        if (keytext("Ctrl")) {
            sch.ctrl = true;
        } else if (keytext("R")) {
            if (sch.state != Schematic.STATE_NODE) {
                sch.rotateSelection(-1);
            } else {
                sch.tnode.rotate(1);
            }
        } else if (keytext("Up")) {
            sch.moveSelection(0, -1);
        } else if (keytext("Down")) {
            sch.moveSelection(0, +1);
        } else if (keytext("Left")) {
            sch.moveSelection(-1, 0);
        } else if (keytext("Right")) {
            sch.moveSelection(+1, 0);
        } else if (keytext("Delete")) {
            sch.deleteSelection();
        } else if (keytext("Escape")) {
            if (sch.state == Schematic.STATE_EDGE) {
                sch.cancelEdge();   
            } else {
                sch.setNormal();
            }
        }
        
        if (sch.ctrl) {
            if (e.getKeyChar() == '+') {
                sch.zoom(Schematic.zoom + 1);
            } else if (e.getKeyChar() == '-' || keytext("Minus")) {
                sch.zoom(Schematic.zoom - 1);
            } else if (keytext("A")) {
                sch.selectAll();
            } else if (keytext("Z")) {
                sch.undo();
            } else if (keytext("Y")) {
                sch.redo();
            } else if (keytext("X")) {
                sch.cut();
            } else if (keytext("C")) {
                sch.copy();
            } else if (keytext("V")) {
                sch.paste();
            }
        }         
        sch.repaint();
    }

    public void keyReleased(KeyEvent e) {
        event = e;
        
        if (keytext("Ctrl")) {
            sch.ctrl = false;
        } else if (keytext("Up")) {
            if (sch.move) sch.finishMoveSelection();
        } else if (keytext("Down")) {
            if (sch.move) sch.finishMoveSelection();
        } else if (keytext("Left")) {
            if (sch.move) sch.finishMoveSelection();
        } else if (keytext("Right")) {
            if (sch.move) sch.finishMoveSelection();
        } else if (keytext("R")) {
            if (sch.rotate) sch.finishRotateSelection();
        }
        sch.repaint();
    }
    
    protected Schematic sch;
    protected KeyEvent event;
}
